點擊進入React源碼調試倉庫。
當render階段完成後,意味著在內存中構建的workInProgress樹所有更新工作已經完成,這包括樹中fiber節點的更新、diff、effectTag的標記、effectList的收集。此時workInProgress樹的完整形態如下:
和current樹相比,它們的結構上固然存在區別,變化的fiber節點也存在於workInProgress樹,但要將這些節點應用到DOM上卻不會循環整棵樹,而是通過循環effectList這個鏈表來實現,這樣保證了只針對有變化的節點做工作。
所以循環effectList鏈表去將有更新的fiber節點應用到頁面上是commit階段的主要工作。
commit階段的入口是commitRoot
函數,它會告知scheduler以立即執行的優先級去調度commit階段的工作。
function commitRoot(root) {
const renderPriorityLevel = getCurrentPriorityLevel();
runWithPriority(
ImmediateSchedulerPriority,
commitRootImpl.bind(null, root, renderPriorityLevel),
);
return null;
}
scheduler去調度的是commitRootImpl
,它是commit階段的核心實現,整個commit階段被劃分成三個部分。
commit階段主要是針對root上收集的effectList進行處理。在真正的工作開始之前,有壹個準備階段,主要是變量的賦值,以及將root的effect加入到effectList中。隨後開始針對effectList分三個階段進行工作:
before mutation和layout針對函數組件的useEffect調度是互斥的,只能發起壹次調度
workInProgress 樹切換到current樹的時機是在mutation結束後,layout開始前。這樣做的原因是在mutation階段調用類組件的componentWillUnmount的時候,
還可以獲取到卸載前的組件信息;在layout階段調用componentDidMount/Update時,獲取的組件信息更新後的。
function commitRootImpl(root, renderPriorityLevel) {
// 進入commit階段,先執行壹次之前未執行的useEffect
do {
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
// 準備階段-----------------------------------------------
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
if (finishedWork === null) {
return null;
}
root.finishedWork = null;
root.finishedLanes = NoLanes;
root.callbackNode = null;
root.callbackId = NoLanes;
// effectList的整理,將root上的effect連到effectList的末尾
let firstEffect;
if (finishedWork.effectTag > PerformedWork) {
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// There is no effect on the root.
firstEffect = finishedWork.firstEffect;
}
// 準備階段結束,開始處理effectList
if (firstEffect !== null) {
...
// before mutation階段--------------------------------
nextEffect = firstEffect;
do {...} while (nextEffect !== null);
...
// mutation階段---------------------------------------
nextEffect = firstEffect;
do {...} while (nextEffect !== null);
// 將wprkInProgress樹切換為current樹
root.current = finishedWork;
// layout階段-----------------------------------------
nextEffect = firstEffect;
do {...} while (nextEffect !== null);
nextEffect = null;
// 通知瀏覽器去繪制
requestPaint();
} else {
// 沒有effectList,直接將wprkInProgress樹切換為current樹
root.current = finishedWork;
}
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
// 獲取尚未處理的優先級,比如之前被跳過的任務的優先級
remainingLanes = root.pendingLanes;
// 將被跳過的優先級放到root上的pendingLanes(待處理的優先級)上
markRootFinished(root, remainingLanes);
/*
* 每次commit階段完成後,再執行壹遍ensureRootIsScheduled,確保是否還有任務需要被調度。
* 例如,高優先級插隊的更新完成後,commit完成後,還會再執行壹遍,保證之前跳過的低優先級任務
* 重新調度
*
* */
ensureRootIsScheduled(root, now());
...
return null;
}
下面的部分,是對這三個階段分別進行的詳細講解。
beforeMutation階段的入口函數是commitBeforeMutationEffects
nextEffect = firstEffect;
do {
try {
commitBeforeMutationEffects();
} catch (error) {
...
}
} while (nextEffect !== null);
它的作用主要是調用類組件的getSnapshotBeforeUpdate
,針對函數組件,異步調度useEffect。
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const current = nextEffect.alternate;
...
const flags = nextEffect.flags;
if ((flags & Snapshot) !== NoFlags) {
// 通過commitBeforeMutationEffectOnFiber調用getSnapshotBeforeUpdate
commitBeforeMutationEffectOnFiber(current, nextEffect);
}
if ((flags & Passive) !== NoFlags) {
// 異步調度useEffect
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
commitBeforeMutationEffectOnFiber代碼如下
function commitBeforeMutationLifeCycles(
current: Fiber | null,
finishedWork: Fiber,
): void {
switch (finishedWork.tag) {
...
case ClassComponent: {
if (finishedWork.flags & Snapshot) {
if (current !== null) {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
const instance = finishedWork.stateNode;
// 調用getSnapshotBeforeUpdate
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
// 將返回值存儲在內部屬性上,方便componentDidUpdate獲取
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
}
return;
}
...
}
}
mutation階段會真正操作DOM節點,涉及到的操作有增、刪、改。入口函數是commitMutationEffects
nextEffect = firstEffect;
do {
try {
commitMutationEffects(root, renderPriorityLevel);
} catch (error) {
...
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);
由於過程較為復雜,所以我寫了三篇文章來說明這三種DOM操作,如果想要了解細節,可以看壹下。文章寫於17還未正式發布的時候,所以裏面的源碼版本取自17.0.0-alpha0。
layout階段的入口函數是commitLayoutEffects
。
nextEffect = firstEffect;
do {
try {
commitLayoutEffects(root, lanes);
} catch (error) {
...
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);
我們只關註classComponent和functionComponent。針對前者,調用生命周期componentDidMount和componentDidUpdate,調用setState的回調;針對後者,填充useEffect 的 effect執行數組,並調度useEffect(具體的原理在我的這篇文章:梳理useEffect和useLayoutEffect的原理與區別中有講解)。
function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
// 執行useLayoutEffect的創建
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
// 填充useEffect的effect執行數組
schedulePassiveEffects(finishedWork);
return;
}
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {
if (current === null) {
// 如果是初始掛載階段,調用componentDidMount
instance.componentDidMount();
} else {
// 如果是更新階段,調用componentDidUpdate
const prevProps =
finishedWork.elementType === finishedWork.type
? current.memoizedProps
: resolveDefaultProps(finishedWork.type, current.memoizedProps);
const prevState = current.memoizedState;
instance.componentDidUpdate(
prevProps,
prevState,
// 將getSnapshotBeforeUpdate的結果傳入
instance.__reactInternalSnapshotBeforeUpdate,
);
}
}
// 調用setState的回調
const updateQueue: UpdateQueue<
*,
> | null = (finishedWork.updateQueue: any);
if (updateQueue !== null) {
commitUpdateQueue(finishedWork, updateQueue, instance);
}
return;
}
...
}
}
commit階段將effectList的處理分成三個階段保證了不同生命周期函數的適時調用。相對於同步執行的useEffectLayout,useEffect的異步調度提供了壹種不阻塞頁面渲染的副作用操作入口。另外,標記root上還未處理的優先級和調用ensureRootIsScheduled使得被跳過的低優先級任務得以再次被調度。commit階段的完成,也就意味著本次更新已經結束。